3.1 同构应用原理

3.1.1 同构概念深入理解

同构(Isomorphic)应用是指同一套代码可以在服务端和客户端运行的应用程序。在Vue SSR中,这意味着Vue组件既可以在Node.js环境中渲染成HTML字符串,也可以在浏览器中正常运行。

// 同构应用的核心思想
const isomorphicConcept = {
  '一套代码': {
    description: '相同的Vue组件代码',
    serverSide: '在Node.js中执行,生成HTML',
    clientSide: '在浏览器中执行,提供交互'
  },
  '两种环境': {
    server: {
      environment: 'Node.js',
      purpose: '预渲染HTML',
      output: 'HTML字符串 + 初始状态'
    },
    client: {
      environment: 'Browser',
      purpose: '激活交互',
      input: 'HTML + 初始状态'
    }
  }
}

3.1.2 环境差异处理

// src/utils/env.js
// 环境检测工具
export const isServer = typeof window === 'undefined'
export const isClient = typeof window !== 'undefined'

// 安全的DOM操作
export function safeDocument() {
  return isClient ? document : null
}

export function safeWindow() {
  return isClient ? window : null
}

// 条件执行
export function clientOnly(fn) {
  if (isClient) {
    return fn()
  }
  return null
}

export function serverOnly(fn) {
  if (isServer) {
    return fn()
  }
  return null
}

3.1.3 通用组件编写

<!-- src/components/UniversalComponent.vue -->
<template>
  <div class="universal-component">
    <h2>{{ title }}</h2>
    <p>当前时间: {{ currentTime }}</p>
    <p>环境: {{ environment }}</p>
    <button v-if="isClient" @click="updateTime">
      更新时间
    </button>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'
import { isClient, isServer } from '@/utils/env'

export default {
  name: 'UniversalComponent',
  props: {
    title: {
      type: String,
      default: '通用组件'
    }
  },
  setup() {
    const currentTime = ref(new Date().toLocaleString())
    const environment = isServer ? 'Server' : 'Client'
    
    const updateTime = () => {
      currentTime.value = new Date().toLocaleString()
    }
    
    // 只在客户端执行
    onMounted(() => {
      console.log('组件已在客户端挂载')
    })
    
    return {
      currentTime,
      environment,
      updateTime,
      isClient
    }
  }
}
</script>

3.2 Vue SSR渲染流程

3.2.1 服务端渲染流程

// server/render.js
import { renderToString } from 'vue/server-renderer'
import { createApp } from '../src/app.js'

export async function renderPage(context) {
  // 1. 创建应用实例
  const { app, router, store } = createApp()
  
  // 2. 设置路由
  await router.push(context.url)
  await router.isReady()
  
  // 3. 检查路由匹配
  const matchedRoute = router.currentRoute.value
  if (!matchedRoute.matched.length) {
    throw new Error('404 - Page Not Found')
  }
  
  // 4. 预取数据
  await prefetchData(matchedRoute, store)
  
  // 5. 渲染应用为HTML字符串
  const html = await renderToString(app)
  
  // 6. 获取初始状态
  const state = store.state
  
  return {
    html,
    state,
    title: matchedRoute.meta?.title || 'Vue SSR App'
  }
}

// 数据预取函数
async function prefetchData(route, store) {
  const matchedComponents = route.matched
    .flatMap(record => Object.values(record.components || {}))
  
  const asyncDataPromises = matchedComponents
    .filter(component => component.asyncData)
    .map(component => component.asyncData({
      store,
      route
    }))
  
  await Promise.all(asyncDataPromises)
}

3.2.2 客户端激活流程

// src/entry-client.js
import { createApp } from './app.js'

const { app, router, store } = createApp()

// 恢复服务端状态
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

// 等待路由准备就绪
router.isReady().then(() => {
  // 添加路由钩子,处理客户端导航
  router.beforeResolve(async (to, from, next) => {
    const matched = router.resolve(to).matched
    const prevMatched = router.resolve(from).matched
    
    // 找出需要预取数据的组件
    let diffed = false
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = (prevMatched[i] !== c))
    })
    
    if (!activated.length) {
      return next()
    }
    
    // 显示加载指示器
    showLoadingIndicator()
    
    try {
      // 预取数据
      await Promise.all(
        activated
          .flatMap(record => Object.values(record.components || {}))
          .filter(component => component.asyncData)
          .map(component => component.asyncData({
            store,
            route: to
          }))
      )
      
      hideLoadingIndicator()
      next()
    } catch (error) {
      hideLoadingIndicator()
      next(error)
    }
  })
  
  // 挂载应用
  app.mount('#app')
})

function showLoadingIndicator() {
  // 显示加载动画
  console.log('Loading...')
}

function hideLoadingIndicator() {
  // 隐藏加载动画
  console.log('Loaded')
}

3.3 状态管理与同步

3.3.1 Vuex状态管理

// src/store/index.js
import { createStore } from 'vuex'
import { userModule } from './modules/user'
import { postsModule } from './modules/posts'

export function createStore() {
  return createStore({
    modules: {
      user: userModule,
      posts: postsModule
    },
    strict: process.env.NODE_ENV !== 'production'
  })
}
// src/store/modules/user.js
export const userModule = {
  namespaced: true,
  
  state: () => ({
    currentUser: null,
    isAuthenticated: false,
    profile: null
  }),
  
  mutations: {
    SET_USER(state, user) {
      state.currentUser = user
      state.isAuthenticated = !!user
    },
    
    SET_PROFILE(state, profile) {
      state.profile = profile
    },
    
    CLEAR_USER(state) {
      state.currentUser = null
      state.isAuthenticated = false
      state.profile = null
    }
  },
  
  actions: {
    async fetchUser({ commit }, userId) {
      try {
        const response = await fetch(`/api/users/${userId}`)
        const user = await response.json()
        commit('SET_USER', user)
        return user
      } catch (error) {
        console.error('Failed to fetch user:', error)
        throw error
      }
    },
    
    async fetchProfile({ commit }, userId) {
      try {
        const response = await fetch(`/api/users/${userId}/profile`)
        const profile = await response.json()
        commit('SET_PROFILE', profile)
        return profile
      } catch (error) {
        console.error('Failed to fetch profile:', error)
        throw error
      }
    }
  },
  
  getters: {
    isLoggedIn: state => state.isAuthenticated,
    userName: state => state.currentUser?.name || 'Guest',
    userAvatar: state => state.profile?.avatar || '/default-avatar.png'
  }
}

3.3.2 状态序列化与反序列化

// src/utils/state.js
import serialize from 'serialize-javascript'

// 序列化状态(服务端)
export function serializeState(state) {
  return serialize(state, { isJSON: true })
}

// 反序列化状态(客户端)
export function deserializeState(serializedState) {
  try {
    return JSON.parse(serializedState)
  } catch (error) {
    console.error('Failed to deserialize state:', error)
    return {}
  }
}

// 状态注入HTML模板
export function injectState(html, state) {
  const serializedState = serializeState(state)
  const stateScript = `
    <script>
      window.__INITIAL_STATE__ = ${serializedState}
    </script>
  `
  
  return html.replace('</body>', `${stateScript}</body>`)
}

3.3.3 数据预取策略

// src/mixins/asyncData.js
export const asyncDataMixin = {
  beforeMount() {
    const { asyncData } = this.$options
    if (asyncData) {
      // 客户端路由切换时预取数据
      this.dataPromise = asyncData({
        store: this.$store,
        route: this.$route
      })
    }
  }
}

// 使用示例
// src/views/UserProfile.vue
export default {
  name: 'UserProfile',
  mixins: [asyncDataMixin],
  
  // 服务端和客户端都会调用
  async asyncData({ store, route }) {
    const userId = route.params.id
    await Promise.all([
      store.dispatch('user/fetchUser', userId),
      store.dispatch('user/fetchProfile', userId)
    ])
  },
  
  computed: {
    user() {
      return this.$store.state.user.currentUser
    },
    profile() {
      return this.$store.state.user.profile
    }
  }
}

3.4 路由配置与管理

3.4.1 路由器创建

// src/router/index.js
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
import { isServer } from '@/utils/env'

// 路由配置
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: {
      title: '首页',
      description: '欢迎来到我们的网站'
    }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue'),
    meta: {
      title: '关于我们',
      description: '了解更多关于我们的信息'
    }
  },
  {
    path: '/users/:id',
    name: 'UserProfile',
    component: () => import('@/views/UserProfile.vue'),
    meta: {
      title: '用户资料',
      requiresAuth: true
    }
  },
  {
    path: '/posts',
    name: 'Posts',
    component: () => import('@/views/Posts.vue'),
    meta: {
      title: '文章列表'
    }
  },
  {
    path: '/posts/:id',
    name: 'PostDetail',
    component: () => import('@/views/PostDetail.vue'),
    meta: {
      title: '文章详情'
    }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue'),
    meta: {
      title: '页面未找到'
    }
  }
]

// 创建路由器
export function createRouter() {
  return createRouter({
    history: isServer ? createMemoryHistory() : createWebHistory(),
    routes
  })
}

3.4.2 路由守卫

// src/router/guards.js
export function setupRouterGuards(router, store) {
  // 全局前置守卫
  router.beforeEach(async (to, from, next) => {
    // 检查认证
    if (to.meta.requiresAuth && !store.getters['user/isLoggedIn']) {
      next({ name: 'Login', query: { redirect: to.fullPath } })
      return
    }
    
    // 设置页面标题
    if (to.meta.title) {
      document.title = `${to.meta.title} - Vue SSR App`
    }
    
    next()
  })
  
  // 全局后置钩子
  router.afterEach((to, from) => {
    // 页面访问统计
    if (typeof gtag !== 'undefined') {
      gtag('config', 'GA_MEASUREMENT_ID', {
        page_path: to.fullPath
      })
    }
  })
}

3.4.3 动态路由

// src/router/dynamic.js
export async function addDynamicRoutes(router, store) {
  try {
    // 从API获取动态路由配置
    const response = await fetch('/api/routes')
    const dynamicRoutes = await response.json()
    
    dynamicRoutes.forEach(route => {
      router.addRoute({
        path: route.path,
        name: route.name,
        component: () => import(`@/views/${route.component}.vue`),
        meta: route.meta || {}
      })
    })
    
    console.log('Dynamic routes added successfully')
  } catch (error) {
    console.error('Failed to add dynamic routes:', error)
  }
}

3.5 组件生命周期处理

3.5.1 SSR生命周期钩子

// SSR环境下的生命周期执行情况
const ssrLifecycleHooks = {
  '服务端执行': [
    'beforeCreate',
    'created'
  ],
  '客户端执行': [
    'beforeMount',
    'mounted',
    'beforeUpdate',
    'updated',
    'beforeUnmount',
    'unmounted'
  ],
  '注意事项': [
    'mounted钩子不在服务端执行',
    '避免在created中访问DOM',
    '定时器需要在客户端清理'
  ]
}

3.5.2 生命周期最佳实践

<!-- src/components/LifecycleDemo.vue -->
<template>
  <div class="lifecycle-demo">
    <h3>生命周期演示</h3>
    <p>组件ID: {{ componentId }}</p>
    <p>挂载时间: {{ mountTime }}</p>
    <p>计数器: {{ counter }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script>
import { ref, onMounted, onUnmounted } from 'vue'
import { isClient } from '@/utils/env'

export default {
  name: 'LifecycleDemo',
  
  setup() {
    const componentId = ref(Math.random().toString(36).substr(2, 9))
    const mountTime = ref(null)
    const counter = ref(0)
    let timer = null
    
    const increment = () => {
      counter.value++
    }
    
    // 只在客户端执行
    onMounted(() => {
      if (isClient) {
        mountTime.value = new Date().toLocaleString()
        
        // 启动定时器
        timer = setInterval(() => {
          console.log('Timer tick:', counter.value)
        }, 5000)
        
        console.log('Component mounted on client')
      }
    })
    
    // 清理定时器
    onUnmounted(() => {
      if (timer) {
        clearInterval(timer)
        timer = null
      }
    })
    
    return {
      componentId,
      mountTime,
      counter,
      increment
    }
  }
}
</script>

3.5.3 异步组件处理

// src/components/AsyncComponentWrapper.vue
import { defineAsyncComponent } from 'vue'
import LoadingComponent from './LoadingComponent.vue'
import ErrorComponent from './ErrorComponent.vue'

export default {
  name: 'AsyncComponentWrapper',
  
  components: {
    AsyncComponent: defineAsyncComponent({
      loader: () => import('./HeavyComponent.vue'),
      loadingComponent: LoadingComponent,
      errorComponent: ErrorComponent,
      delay: 200,
      timeout: 3000,
      suspensible: false
    })
  },
  
  template: `
    <div class="async-wrapper">
      <Suspense>
        <template #default>
          <AsyncComponent />
        </template>
        <template #fallback>
          <div>Loading async component...</div>
        </template>
      </Suspense>
    </div>
  `
}

3.6 错误处理与调试

3.6.1 服务端错误处理

// server/errorHandler.js
export function createErrorHandler() {
  return (err, req, res, next) => {
    console.error('SSR Error:', err)
    
    // 根据错误类型返回不同响应
    if (err.code === 404) {
      res.status(404).send(`
        <!DOCTYPE html>
        <html>
        <head><title>页面未找到</title></head>
        <body>
          <h1>404 - 页面未找到</h1>
          <p>抱歉,您访问的页面不存在。</p>
          <a href="/">返回首页</a>
        </body>
        </html>
      `)
    } else if (err.code === 500 || !err.code) {
      res.status(500).send(`
        <!DOCTYPE html>
        <html>
        <head><title>服务器错误</title></head>
        <body>
          <h1>500 - 服务器内部错误</h1>
          <p>服务器遇到了一个错误,请稍后再试。</p>
          <a href="/">返回首页</a>
        </body>
        </html>
      `)
    } else {
      next(err)
    }
  }
}

3.6.2 客户端错误处理

// src/utils/errorHandler.js
export function setupErrorHandling(app) {
  // Vue错误处理
  app.config.errorHandler = (err, instance, info) => {
    console.error('Vue Error:', err)
    console.error('Component:', instance)
    console.error('Info:', info)
    
    // 发送错误报告
    reportError({
      error: err.message,
      stack: err.stack,
      component: instance?.$options.name,
      info
    })
  }
  
  // 全局未捕获错误
  if (typeof window !== 'undefined') {
    window.addEventListener('error', (event) => {
      console.error('Global Error:', event.error)
      reportError({
        error: event.error.message,
        stack: event.error.stack,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno
      })
    })
    
    // Promise未捕获错误
    window.addEventListener('unhandledrejection', (event) => {
      console.error('Unhandled Promise Rejection:', event.reason)
      reportError({
        error: 'Unhandled Promise Rejection',
        reason: event.reason
      })
    })
  }
}

function reportError(errorInfo) {
  // 发送错误报告到监控服务
  if (process.env.NODE_ENV === 'production') {
    fetch('/api/errors', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        ...errorInfo,
        timestamp: new Date().toISOString(),
        userAgent: navigator.userAgent,
        url: window.location.href
      })
    }).catch(err => {
      console.error('Failed to report error:', err)
    })
  }
}

3.6.3 调试工具配置

// src/utils/debug.js
export function setupDebugTools() {
  if (process.env.NODE_ENV === 'development' && typeof window !== 'undefined') {
    // 添加调试信息到window对象
    window.__VUE_SSR_DEBUG__ = {
      version: process.env.VUE_APP_VERSION,
      buildTime: process.env.VUE_APP_BUILD_TIME,
      environment: process.env.NODE_ENV
    }
    
    // 性能监控
    if (window.performance) {
      window.addEventListener('load', () => {
        setTimeout(() => {
          const perfData = window.performance.timing
          const loadTime = perfData.loadEventEnd - perfData.navigationStart
          console.log(`Page load time: ${loadTime}ms`)
          
          // 发送性能数据
          if (navigator.sendBeacon) {
            navigator.sendBeacon('/api/performance', JSON.stringify({
              loadTime,
              domContentLoaded: perfData.domContentLoadedEventEnd - perfData.navigationStart,
              firstPaint: performance.getEntriesByType('paint')[0]?.startTime
            }))
          }
        }, 0)
      })
    }
  }
}

3.7 性能优化基础

3.7.1 组件缓存

// src/utils/componentCache.js
import LRU from 'lru-cache'

// 创建组件缓存
const componentCache = new LRU({
  max: 1000,
  ttl: 1000 * 60 * 15 // 15分钟
})

export function getCachedComponent(key) {
  return componentCache.get(key)
}

export function setCachedComponent(key, component) {
  componentCache.set(key, component)
}

export function clearComponentCache() {
  componentCache.clear()
}

// 缓存装饰器
export function withCache(component, cacheKey) {
  return {
    ...component,
    __cacheKey: cacheKey,
    __cached: true
  }
}

3.7.2 资源预加载

// src/utils/preload.js
export function preloadRoute(routePath) {
  const router = this.$router
  const route = router.resolve(routePath)
  
  if (route.matched.length) {
    route.matched.forEach(record => {
      Object.values(record.components || {}).forEach(component => {
        if (typeof component === 'function') {
          // 预加载异步组件
          component()
        }
      })
    })
  }
}

export function preloadResource(href, as = 'script') {
  if (typeof document !== 'undefined') {
    const link = document.createElement('link')
    link.rel = 'preload'
    link.href = href
    link.as = as
    document.head.appendChild(link)
  }
}

// 智能预加载
export function setupIntelligentPreloading(router) {
  if (typeof window !== 'undefined' && 'IntersectionObserver' in window) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const link = entry.target
          const href = link.getAttribute('href')
          if (href && href.startsWith('/')) {
            preloadRoute(href)
            observer.unobserve(link)
          }
        }
      })
    })
    
    // 观察所有内部链接
    document.addEventListener('DOMContentLoaded', () => {
      document.querySelectorAll('a[href^="/"]').forEach(link => {
        observer.observe(link)
      })
    })
  }
}

3.8 本章小结

3.8.1 核心概念回顾

  1. 同构应用:一套代码,两端运行,需要处理环境差异
  2. 渲染流程:服务端预渲染HTML,客户端激活交互
  3. 状态管理:服务端和客户端状态同步,数据预取策略
  4. 路由管理:支持服务端和客户端路由,动态路由配置
  5. 生命周期:理解SSR环境下的组件生命周期差异

3.8.2 最佳实践

  • 使用环境检测避免服务端DOM操作
  • 合理设计数据预取策略
  • 实现完善的错误处理机制
  • 配置组件缓存提升性能
  • 使用智能预加载优化用户体验

3.8.3 下章预告

下一章我们将深入学习路由与导航的高级特性: - 嵌套路由配置 - 路由懒加载 - 导航守卫详解 - 动态路由匹配 - 路由元信息应用


练习作业:

  1. 创建一个包含数据预取的页面组件
  2. 实现一个通用的错误处理组件
  3. 配置路由守卫,实现权限控制
  4. 添加组件缓存,测试性能提升效果

下一章: 路由与导航